Просмотр исходного кода

Replace Core Data publisher in our Services (TP, NS, Health)

Marvin Polscheit недель назад: 2
Родитель
Сommit
6ce76424d0

+ 79 - 44
Trio/Sources/Services/HealthKit/HealthKitManager.swift

@@ -55,10 +55,65 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var deviceDataManager: DeviceDataManager!
 
-    // Queue for handling Core Data change notifications
-    private let queue = DispatchQueue(label: "BaseHealthKitManager.queue", qos: .background)
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
-    private var subscriptions = Set<AnyCancellable>()
+    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+
+    // MARK: - Upload triggers
+
+    //
+    // Each upload pipeline is driven by an NSFetchedResultsController whose predicate is the
+    // "not yet uploaded to Health" set. The controller fires whenever un-uploaded items appear
+    // (or drop out after a successful upload), which we use to (re-)trigger the matching upload.
+    // Bound to the viewContext, it also picks up batch-inserted glucose via the persistent history
+    // merge in CoreDataStack — replacing the previous changedObjects publisher plus the separate
+    // glucoseStorage.updatePublisher fallback.
+
+    let glucoseUploadControllerDelegate = FetchedResultsControllerDelegate()
+    private lazy var glucoseUploadController: NSFetchedResultsController<GlucoseStored> = {
+        let request = NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: true)]
+        request.predicate = NSPredicate.glucoseNotYetUploadedToHealth
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = glucoseUploadControllerDelegate
+        return controller
+    }()
+
+    let carbsUploadControllerDelegate = FetchedResultsControllerDelegate()
+    private lazy var carbsUploadController: NSFetchedResultsController<CarbEntryStored> = {
+        let request = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: true)]
+        request.predicate = NSPredicate.carbsNotYetUploadedToHealth
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = carbsUploadControllerDelegate
+        return controller
+    }()
+
+    let insulinUploadControllerDelegate = FetchedResultsControllerDelegate()
+    private lazy var insulinUploadController: NSFetchedResultsController<PumpEventStored> = {
+        let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: true)]
+        request.predicate = NSPredicate.pumpEventsNotYetUploadedToHealth
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = insulinUploadControllerDelegate
+        return controller
+    }()
 
     var isAvailableOnCurrentDevice: Bool {
         HKHealthStore.isHealthDataAvailable()
@@ -67,23 +122,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     init(resolver: Resolver) {
         injectServices(resolver)
 
-        coreDataPublisher =
-            changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: queue)
-                .share()
-                .eraseToAnyPublisher()
-
-        glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadGlucose()
-                }
-            }
-            .store(in: &subscriptions)
-
-        registerHandlers()
+        registerUploadControllers()
 
         guard isAvailableOnCurrentDevice,
               AppleHealthConfig.healthBGObject != nil else { return }
@@ -91,31 +130,27 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         debug(.service, "HealthKitManager did create")
     }
 
-    private func registerHandlers() {
-        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task { [weak self] in
-                guard let self = self else { return }
-                await self.uploadInsulin()
-            }
-        }.store(in: &subscriptions)
+    private func registerUploadControllers() {
+        glucoseUploadControllerDelegate.onContentChange = { [weak self] in
+            Task { await self?.uploadGlucose() }
+        }
+        carbsUploadControllerDelegate.onContentChange = { [weak self] in
+            Task { await self?.uploadCarbs() }
+        }
+        insulinUploadControllerDelegate.onContentChange = { [weak self] in
+            Task { await self?.uploadInsulin() }
+        }
 
-        coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task { [weak self] in
-                guard let self = self else { return }
-                await self.uploadCarbs()
-            }
-        }.store(in: &subscriptions)
-
-        // This works only for manual Glucose
-        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task { [weak self] in
-                guard let self = self else { return }
-                await self.uploadGlucose()
+        // performFetch must run on the viewContext's queue (main).
+        Task { @MainActor in
+            do {
+                try self.glucoseUploadController.performFetch()
+                try self.carbsUploadController.performFetch()
+                try self.insulinUploadController.performFetch()
+            } catch {
+                debug(.service, "\(DebuggingIdentifiers.failed) Failed to set up HealthKit upload controllers: \(error)")
             }
-        }.store(in: &subscriptions)
+        }
     }
 
     func checkWriteToHealthPermissions(objectTypeToHealthStore: HKObjectType) -> Bool {

+ 45 - 56
Trio/Sources/Services/Network/Nightscout/BaseNightscoutManager+Subscribers.swift

@@ -5,12 +5,10 @@ import Foundation
 extension BaseNightscoutManager {
     /// Call once from init. Hooks up:
     /// 1) external upload requests (NotificationCenter)
-    /// 2) Core Data change triggers → requests per upload pipeline
-    /// 3) Glucose storage updates → request glucose pipeline
+    /// 2) Core Data "not yet uploaded" triggers → requests per upload pipeline
     func wireSubscribers() {
         wireExternalUploadRequests()
-        wireCoreDataSubscribers()
-        wireGlucoseStorageSubscriber()
+        wireUploadControllers()
     }
 
     /// Listens for `.nightscoutUploadRequested`, converts userInfo pipelines to enums,
@@ -31,59 +29,50 @@ extension BaseNightscoutManager {
             .store(in: &subscriptions)
     }
 
-    /// Maps Core Data entity changes into upload pipeline requests. We rely on
-    /// per-pipeline throttle so rapid changes don’t spam Nightscout.
-    func wireCoreDataSubscribers() {
-        coreDataPublisher?
-            .filteredByEntityName("OrefDetermination")
-            .sink { [weak self] _ in self?.requestUpload(.deviceStatus) }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?
-            .filteredByEntityName("OverrideStored")
-            .sink { [weak self] _ in self?.requestUpload(.overrides) }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?
-            .filteredByEntityName("OverrideRunStored")
-            .sink { [weak self] _ in self?.requestUpload(.overrides) }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?
-            .filteredByEntityName("TempTargetStored")
-            .sink { [weak self] _ in self?.requestUpload(.tempTargets) }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?
-            .filteredByEntityName("TempTargetRunStored")
-            .sink { [weak self] _ in self?.requestUpload(.tempTargets) }
-            .store(in: &subscriptions)
+    /// Maps Core Data "not yet uploaded to Nightscout" sets to upload pipeline requests via
+    /// NSFetchedResultsControllers. Each controller fires when un-uploaded items appear (or drop
+    /// out after a successful upload). We rely on the per-pipeline throttle so rapid changes
+    /// don't spam Nightscout.
+    func wireUploadControllers() {
+        determinationUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.deviceStatus)
+        }
+        overrideUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.overrides)
+        }
+        overrideRunUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.overrides)
+        }
+        tempTargetUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.tempTargets)
+        }
+        tempTargetRunUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.tempTargets)
+        }
+        pumpEventUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.pumpHistory)
+        }
+        carbEntryUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.carbs)
+        }
+        glucoseUploadControllerDelegate.onContentChange = { [weak self] in
+            self?.requestUpload(.glucose)
+        }
 
-        coreDataPublisher?
-            .filteredByEntityName("PumpEventStored")
-            .sink { [weak self] _ in self?.requestUpload(.pumpHistory) }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?
-            .filteredByEntityName("CarbEntryStored")
-            .sink { [weak self] _ in self?.requestUpload(.carbs) }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?
-            .filteredByEntityName("GlucoseStored")
-            .sink { [weak self] _ in
-                self?.requestUpload(.glucose)
+        // performFetch must run on the viewContext's queue (main).
+        Task { @MainActor in
+            do {
+                try self.determinationUploadController.performFetch()
+                try self.overrideUploadController.performFetch()
+                try self.overrideRunUploadController.performFetch()
+                try self.tempTargetUploadController.performFetch()
+                try self.tempTargetRunUploadController.performFetch()
+                try self.pumpEventUploadController.performFetch()
+                try self.carbEntryUploadController.performFetch()
+                try self.glucoseUploadController.performFetch()
+            } catch {
+                debug(.nightscout, "\(DebuggingIdentifiers.failed) Failed to set up Nightscout upload controllers: \(error)")
             }
-            .store(in: &subscriptions)
-    }
-
-    /// Glucose storage updates → request glucose pipeline
-    func wireGlucoseStorageSubscriber() {
-        glucoseStorage.updatePublisher
-            .receive(on: queue)
-            .sink { [weak self] _ in
-                self?.requestUpload(.glucose)
-            }
-            .store(in: &subscriptions)
+        }
     }
 }

+ 161 - 13
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -126,26 +126,174 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private var lastEnactedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
 
-    // Queue for handling Core Data change notifications
-    let queue = DispatchQueue(label: "BaseNightscoutManager.queue", qos: .utility)
-
-    /// Emits changed Core Data object IDs from the app. We filter by entity names
-    /// and request upload pipelines based on what changed.
-    var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
-
     /// Bag for Combine subscriptions owned by this manager.
     var subscriptions = Set<AnyCancellable>()
 
+    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+
+    // MARK: - Upload triggers
+
+    //
+    // Each upload pipeline is driven by an NSFetchedResultsController whose predicate is the
+    // "not yet uploaded to Nightscout" set for that entity. The controller fires whenever
+    // un-uploaded items appear (or drop out after a successful upload), which we map to a
+    // `requestUpload(pipeline)` call (throttled per pipeline). Bound to the viewContext, they
+    // also pick up batch-inserted glucose via the persistent history merge in CoreDataStack —
+    // replacing the previous changedObjects publisher plus the glucoseStorage.updatePublisher
+    // fallback.
+
+    let determinationUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var determinationUploadController: NSFetchedResultsController<OrefDetermination> = {
+        let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: true)]
+        request.predicate = NSPredicate(
+            format: "deliverAt >= %@ AND isUploadedToNS == %@",
+            Date.oneDayAgo as NSDate,
+            false as NSNumber
+        )
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = determinationUploadControllerDelegate
+        return controller
+    }()
+
+    let overrideUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var overrideUploadController: NSFetchedResultsController<OverrideStored> = {
+        let request = NSFetchRequest<OverrideStored>(entityName: "OverrideStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \OverrideStored.date, ascending: true)]
+        request.predicate = NSPredicate(
+            format: "date >= %@ AND isUploadedToNS == %@",
+            Date.oneDayAgo as NSDate,
+            false as NSNumber
+        )
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = overrideUploadControllerDelegate
+        return controller
+    }()
+
+    let overrideRunUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var overrideRunUploadController: NSFetchedResultsController<OverrideRunStored> = {
+        let request = NSFetchRequest<OverrideRunStored>(entityName: "OverrideRunStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \OverrideRunStored.startDate, ascending: true)]
+        request.predicate = NSPredicate(
+            format: "startDate >= %@ AND isUploadedToNS == %@",
+            Date.oneDayAgo as NSDate,
+            false as NSNumber
+        )
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = overrideRunUploadControllerDelegate
+        return controller
+    }()
+
+    let tempTargetUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var tempTargetUploadController: NSFetchedResultsController<TempTargetStored> = {
+        let request = NSFetchRequest<TempTargetStored>(entityName: "TempTargetStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \TempTargetStored.date, ascending: true)]
+        request.predicate = NSPredicate(
+            format: "date >= %@ AND isUploadedToNS == %@",
+            Date.oneDayAgo as NSDate,
+            false as NSNumber
+        )
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = tempTargetUploadControllerDelegate
+        return controller
+    }()
+
+    let tempTargetRunUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var tempTargetRunUploadController: NSFetchedResultsController<TempTargetRunStored> = {
+        let request = NSFetchRequest<TempTargetRunStored>(entityName: "TempTargetRunStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \TempTargetRunStored.startDate, ascending: true)]
+        request.predicate = NSPredicate(
+            format: "startDate >= %@ AND isUploadedToNS == %@",
+            Date.oneDayAgo as NSDate,
+            false as NSNumber
+        )
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = tempTargetRunUploadControllerDelegate
+        return controller
+    }()
+
+    let pumpEventUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var pumpEventUploadController: NSFetchedResultsController<PumpEventStored> = {
+        let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: true)]
+        request.predicate = NSPredicate.pumpEventsNotYetUploadedToNightscout
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = pumpEventUploadControllerDelegate
+        return controller
+    }()
+
+    let carbEntryUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var carbEntryUploadController: NSFetchedResultsController<CarbEntryStored> = {
+        let request = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: true)]
+        request.predicate = NSPredicate(
+            format: "date >= %@ AND isUploadedToNS == %@",
+            Date.oneDayAgo as NSDate,
+            false as NSNumber
+        )
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = carbEntryUploadControllerDelegate
+        return controller
+    }()
+
+    let glucoseUploadControllerDelegate = FetchedResultsControllerDelegate()
+    lazy var glucoseUploadController: NSFetchedResultsController<GlucoseStored> = {
+        let request = NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: true)]
+        request.predicate = NSPredicate.glucoseNotYetUploadedToNightscout
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = glucoseUploadControllerDelegate
+        return controller
+    }()
+
     init(resolver: Resolver) {
         injectServices(resolver)
         subscribe()
 
-        coreDataPublisher =
-            changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: queue)
-                .share()
-                .eraseToAnyPublisher()
-
         setupNotification()
 
         setupLanePipelines()

+ 82 - 48
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -39,10 +39,65 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
-    // Queue for handling Core Data change notifications
-    private let queue = DispatchQueue(label: "BaseTidepoolManager.queue", qos: .background)
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
-    private var subscriptions = Set<AnyCancellable>()
+    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+
+    // MARK: - Upload triggers
+
+    //
+    // Each upload pipeline is driven by an NSFetchedResultsController whose predicate is the
+    // "not yet uploaded to Tidepool" set. The controller fires whenever un-uploaded items appear
+    // (or drop out after a successful upload), which we use to (re-)trigger the matching upload.
+    // Bound to the viewContext, it also picks up batch-inserted glucose via the persistent history
+    // merge in CoreDataStack — replacing the previous changedObjects publisher plus the separate
+    // glucoseStorage.updatePublisher fallback.
+
+    let glucoseUploadControllerDelegate = FetchedResultsControllerDelegate()
+    private lazy var glucoseUploadController: NSFetchedResultsController<GlucoseStored> = {
+        let request = NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: true)]
+        request.predicate = NSPredicate.glucoseNotYetUploadedToTidepool
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = glucoseUploadControllerDelegate
+        return controller
+    }()
+
+    let carbsUploadControllerDelegate = FetchedResultsControllerDelegate()
+    private lazy var carbsUploadController: NSFetchedResultsController<CarbEntryStored> = {
+        let request = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: true)]
+        request.predicate = NSPredicate.carbsNotYetUploadedToTidepool
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = carbsUploadControllerDelegate
+        return controller
+    }()
+
+    let insulinUploadControllerDelegate = FetchedResultsControllerDelegate()
+    private lazy var insulinUploadController: NSFetchedResultsController<PumpEventStored> = {
+        let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: true)]
+        request.predicate = NSPredicate.pumpEventsNotYetUploadedToTidepool
+        request.fetchBatchSize = 50
+        let controller = NSFetchedResultsController(
+            fetchRequest: request,
+            managedObjectContext: viewContext,
+            sectionNameKeyPath: nil,
+            cacheName: nil
+        )
+        controller.delegate = insulinUploadControllerDelegate
+        return controller
+    }()
 
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
@@ -50,23 +105,30 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         injectServices(resolver)
         loadTidepoolManager()
 
-        coreDataPublisher =
-            changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: queue)
-                .share()
-                .eraseToAnyPublisher()
-
-        glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadGlucose()
-                }
-            }
-            .store(in: &subscriptions)
+        registerUploadControllers()
+    }
+
+    private func registerUploadControllers() {
+        glucoseUploadControllerDelegate.onContentChange = { [weak self] in
+            Task { await self?.uploadGlucose() }
+        }
+        carbsUploadControllerDelegate.onContentChange = { [weak self] in
+            Task { await self?.uploadCarbs() }
+        }
+        insulinUploadControllerDelegate.onContentChange = { [weak self] in
+            Task { await self?.uploadInsulin() }
+        }
 
-        registerHandlers()
+        // performFetch must run on the viewContext's queue (main).
+        Task { @MainActor in
+            do {
+                try self.glucoseUploadController.performFetch()
+                try self.carbsUploadController.performFetch()
+                try self.insulinUploadController.performFetch()
+            } catch {
+                debug(.service, "\(DebuggingIdentifiers.failed) Failed to set up Tidepool upload controllers: \(error)")
+            }
+        }
     }
 
     /// Loads the Tidepool service from saved state
@@ -105,34 +167,6 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         return nil
     }
 
-    /// Registers handlers for Core Data changes
-    private func registerHandlers() {
-        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task { [weak self] in
-                guard let self = self else { return }
-                await self.uploadInsulin()
-            }
-        }.store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task { [weak self] in
-                guard let self = self else { return }
-                await self.uploadCarbs()
-            }
-        }.store(in: &subscriptions)
-
-        // This works only for manual Glucose
-        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task { [weak self] in
-                guard let self = self else { return }
-                await self.uploadGlucose()
-            }
-        }.store(in: &subscriptions)
-    }
-
     func sourceInfo() -> [String: Any]? {
         nil
     }